﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Duellum.Core;
using Duellum.Map;
using JpLabs.Geometry;
using System.Windows.Documents;
using JpLabs.Extensions;
//using Duellum.Core.ExceptionHelper;

namespace Duellum.Wpf2d.Plugin
{
    //public class MapCanvas : Canvas, IAddChild
	//[ContentProperty("Children")]
    public class MapScrollViewer : ScrollViewer
    {
        static public readonly DependencyProperty MapUriProperty
			= DependencyProperty.Register(
				"MapUri",
				typeof(Uri),
				typeof(MapScrollViewer),
				new UIPropertyMetadata(MapUri_Changed) { IsAnimationProhibited = true }
			);
        static public readonly DependencyProperty MapProperty
			= DependencyProperty.Register(
				"Map",
				typeof(DuelMap),
				typeof(MapScrollViewer),
				new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, Map_Changed)
				{ IsAnimationProhibited = true }
			);
        static public readonly DependencyProperty LevelProperty
			= DependencyProperty.Register(
				"Level",
				typeof(int),
				typeof(MapScrollViewer),
				new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.Inherits, Level_Changed)
				//new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.Inherits, Level_Changed, Level_Coerced)
				{ IsAnimationProhibited = true }
			);
        static public readonly DependencyProperty ZoomProperty
			= DependencyProperty.Register(
				"Zoom",
				typeof(double),
				typeof(MapScrollViewer),
				new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.Inherits, Zoom_Changed)
				{ IsAnimationProhibited = true }
			);

		public MapScrollViewer() : base()
		{
			this.Initialized += MapViewer_Initialized;
			this.Loaded		 += MapViewer_Loaded;
			
			this.ClipToBounds = true;
			
			this.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
			this.HorizontalScrollBarVisibility = ScrollBarVisibility.Visible;

			this.VisualHost = new MapVisualHost();
			base.AddChild(this.VisualHost);
			
		}

		public DuelDomain DuelDomain { get; set; }
		
		public MapVisualHost VisualHost { get; private set; }

        public Uri MapUri
        {
            get { return (Uri)GetValue(MapUriProperty); }
            set { SetValue(MapUriProperty, value); }
        }
        public DuelMap Map
        {
            get { return (DuelMap)GetValue(MapProperty); }
            set { SetValue(MapProperty, value); }
        }
        public int Level
        {
            get { return (int)GetValue(LevelProperty); }
            set { SetValue(LevelProperty, value); }
        }
        public double Zoom
        {
            get { return (double)GetValue(ZoomProperty); }
            set { SetValue(ZoomProperty, value); }
        }

		static public readonly RoutedEvent LevelChangedEvent
			= EventManager.RegisterRoutedEvent(
				"LevelChanged",
				RoutingStrategy.Bubble,
				typeof(RoutedPropertyChangedEventHandler<int>),
				typeof(MapScrollViewer)
			);
		public event RoutedPropertyChangedEventHandler<int> LevelChanged
		{
			add { AddHandler(LevelChangedEvent, value); } 
			remove { RemoveHandler(LevelChangedEvent, value); }
		}
		
		private void MapViewer_Loaded(object sender, EventArgs e)
		{
			this.FocusVisualStyle = null;

			FocusManager.SetIsFocusScope(this, true);
			FocusManager.SetFocusedElement(this, this);
		}
		
		private void MapViewer_Initialized(object sender, EventArgs e)
		{
			//var bg = new ImageBrush();
			//bg.ImageSource = new BitmapImage(new Uri("pack://application:,,,/res/hex_repeat.bmp"));
			//bg.TileMode = TileMode.Tile;
			//bg.Viewport = new Rect(0, 0, 38, 22);
			//bg.ViewportUnits = BrushMappingMode.Absolute;
			//this.Background = bg;
			
			this.VisualHost.ToolTipOpening += MapVisual_ToolTipOpening;
			
			this.AddRenderer(new MapRenderer(this));
			
			if (this.MapUri != null && this.DuelDomain != null) this.Map = LoadMap(this.DuelDomain, this.MapUri);

			//void ExecutedRoutedEventHandler(object sender, ExecutedRoutedEventArgs e);
			//void CanExecuteRoutedEventHandler(object sender, CanExecuteRoutedEventArgs e);
			
			this.CommandBindings.Add(new CommandBinding(
			    ComponentCommands.MoveToPageUp,
			    (s, ev) => {this.Level++;},
			    (s, ev) => {ev.CanExecute = (Map == null) ? false : Level < Map.Box.Upper.Z;}
			));
			this.CommandBindings.Add(new CommandBinding(
			    ComponentCommands.MoveToPageDown,
			    (s, ev) => {this.Level--;},
			    (s, ev) => {ev.CanExecute = (Map == null) ? false : Level > Map.Box.Lower.Z;}
			));
			
			const double ZoomRatio = 1.2247448713915889;//Math.Sqrt(1.5);
			const double ZoomMin = 0.512;
			const double ZoomMax = 3;
			Func<double,double> anonymousCoerceZoom = ((double v) => CoerceZoom(v, ZoomRatio, ZoomMin, ZoomMax));
			
			this.CommandBindings.Add(new CommandBinding(
			    NavigationCommands.DecreaseZoom,
			    (s, ev) => {Zoom = anonymousCoerceZoom(Zoom / ZoomRatio);},
			    (s, ev) => {ev.CanExecute = (Zoom > ZoomMin);}
			));
			this.CommandBindings.Add(new CommandBinding(
			    NavigationCommands.IncreaseZoom,
			    (s, ev) => {Zoom = anonymousCoerceZoom(Zoom * ZoomRatio);},
			    (s, ev) => {ev.CanExecute = (Zoom < ZoomMax);}
			));
			this.CommandBindings.Add(new CommandBinding(
			    NavigationCommands.Zoom,
			    (s, ev) => {
					if (ev.Parameter is double) this.Zoom = anonymousCoerceZoom((double)ev.Parameter);
					else this.ClearValue(ZoomProperty);
				},
			    (s, ev) => {ev.CanExecute = true;}
			));

			NavigationCommands.IncreaseZoom.InputGestures.Add(new KeyGesture(Key.Add));
			NavigationCommands.DecreaseZoom.InputGestures.Add(new KeyGesture(Key.Subtract));
			NavigationCommands.Zoom.InputGestures.Add(new KeyGesture(Key.Multiply));
		}
		
		private static double CoerceZoom(double value, double baseRatio, double min, double max)
		{
			//Only a few values are valid in the interval
			return baseRatio.Pow(value.Log(baseRatio).Round()).Min(max).Max(min);
		}
		
		public virtual void InvalidateMapVisual()
		{
			this.VisualHost.InvalidateVisual();
		}

		public void AddAdorner(Adorner adorner)
		{
			//TODO: research Adorners and substitute map renderers with them
			//System.Windows.Documents.Adorner
			//System.Windows.Documents.AdornerLayer
			//http://msdn.microsoft.com/en-us/library/ms743737.aspx
			//this.VisualHost.AddRenderer(renderer);
			var adLayer = AdornerLayer.GetAdornerLayer(this.VisualHost);
			adLayer.Add(adorner);
		}

		public void AddRenderer(IMapRenderer renderer)
		{
			this.VisualHost.AddRenderer(renderer);
		}

		public void RemoveRenderer(IMapRenderer renderer)
		{
			this.VisualHost.RemoveRenderer(renderer);
		}

		public void FocusAt(PointInt position)
		{
			this.Level = position.Z;
			
			var p = Wpf2dPlugin.PositionToCenterPoint(position);
			p = new Point(p.X * Zoom, p.Y * Zoom);
			FocusAt(p);
		}

		public void FocusAt(PointInt primaryPos, PointInt secondaryPos)
		{
			this.Level = primaryPos.Z;
			
			var p1 = Wpf2dPlugin.PositionToCenterPoint(primaryPos);
			var p2 = Wpf2dPlugin.PositionToCenterPoint(secondaryPos);

			p1 = new Point(p1.X * Zoom, p1.Y * Zoom);
			p2 = new Point(p2.X * Zoom, p2.Y * Zoom);
			FocusAt(p1, p2);
		}
		
		public void FocusAt(Point point)
		{
			System.Diagnostics.Debug.Print(IsMeasureValid.ToString());
			
			if (!IsMeasureValid) {
				//It's not possible to find the right scroll values before the control is measured.
				//So, the actual scroll is scheduled to the next time LayoutUpdated happens
				
				EventHandler handler = null;
				handler = (object sender, EventArgs e) =>
				{
					this.LayoutUpdated -= handler;
					
					ScrollTo(point);
				};
				this.LayoutUpdated += handler;
			} else {
				ScrollTo(point);
			}
		}

		private void ScrollTo(Point point)
		{
			this.ScrollToHorizontalOffset(point.X - (this.ViewportWidth / 2));
			this.ScrollToVerticalOffset(point.Y - (this.ViewportHeight / 2));
		}

		public void FocusAt(Point primaryPoint, Point secondaryPoint)
		{
			var diff = secondaryPoint - primaryPoint;
			
			if (Math.Abs(diff.X) > this.ViewportWidth || Math.Abs(diff.Y) > this.ViewportHeight) {
			    FocusAt(primaryPoint);
			} else {
				var center = primaryPoint + (diff / 2);
				FocusAt(center);
			}
		}
		
		private void MapVisual_ToolTipOpening(object sender, ToolTipEventArgs e)
		{
			var mouse = Mouse.GetPosition(this.VisualHost);
			var pos = Wpf2dPlugin.PointToPosition(mouse);
			
			string tooltipText = null;
			if (Map != null) {
			    var objs = Map.Query(pos.X, pos.Y, Level).ToArray();
			    if (objs.Any()) {
					tooltipText = objs
						.Select(o => o.Object.ToString())
						.Join(Environment.NewLine);
			    }
			}
			
			if (tooltipText != null) {
			    var tt = (ToolTip)this.VisualHost.ToolTip;
			    tt.Content = tooltipText;
			} else {
			    e.Handled = true;
			}
		}
		
		protected override void OnScrollChanged(ScrollChangedEventArgs e)
		{
			if (e.HorizontalChange != 0) {
				this.ScrollToHorizontalOffset(Math.Round(e.HorizontalOffset));
				e.Handled = true;
			}
			if (e.VerticalChange != 0) {
			    this.ScrollToVerticalOffset(Math.Round(e.VerticalOffset));
			    e.Handled = true;
			}
			if (!e.Handled) base.OnScrollChanged(e);
		}

		private static DuelMap LoadMap(DuelDomain domain, Uri mapUri)
		{
			return DuelMap.LoadMap(domain, mapUri);
		}

		private static void MapUri_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var me = d as MapScrollViewer; if (me != null) me.MapUri_Changed(e);
		}
		private static void Map_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var me = d as MapScrollViewer; if (me != null) me.Map_Changed(e);
		}
		private static void Level_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var me = d as MapScrollViewer; if (me != null) me.Level_Changed(e);
		}
		private static void Zoom_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var me = d as MapScrollViewer; if (me != null) me.Zoom_Changed(e);
		}

		private void MapUri_Changed(DependencyPropertyChangedEventArgs e)
		{
			if (this.IsInitialized && e.NewValue != e.OldValue) {
				this.Map = LoadMap(this.DuelDomain, this.MapUri);
			}
		}

		private void Map_Changed(DependencyPropertyChangedEventArgs e)
		{
			if (this.IsInitialized && e.NewValue != e.OldValue) {
				this.VisualHost.InvalidateMeasure();
				this.VisualHost.InvalidateVisual();
				if (this.Map != null) this.Level = Map.Box.Lower.Z;
			}
		}

		private void Level_Changed(DependencyPropertyChangedEventArgs e)
		{
			base.RaiseEvent(
				new RoutedPropertyChangedEventArgs<int>(
					(int)e.OldValue,
					(int)e.NewValue,
					LevelChangedEvent
				)
			);
		}
				
		private void Zoom_Changed(DependencyPropertyChangedEventArgs e)
		{
			var newTransform = new ScaleTransform(this.Zoom, this.Zoom);

			//If scroll is not at origin, keep center
			if (ScrollInfo.HorizontalOffset > 0 && ScrollInfo.VerticalOffset > 0) {
				//1. Find the scrolled center in UI coordinates
				var offsetOnUI = new Vector(ScrollInfo.HorizontalOffset, ScrollInfo.VerticalOffset);
				var viewportSizeOnUI = new Vector(ScrollInfo.ViewportWidth, ScrollInfo.ViewportHeight);
				var centerOnUI = offsetOnUI + viewportSizeOnUI / 2;
				
				//2. Find the center in map coordinates
				var currentInverse = (Transform)this.VisualHost.LayoutTransform.Inverse;
				var centerOnMap = centerOnUI * currentInverse.Value;
				
				//3. Find the new offset scroll in map coordinates so that the center keeps the same
				var newInverse = (Transform)newTransform.Inverse;
				var newViewportSizeOnMap = viewportSizeOnUI * newInverse.Value;
				var newOffsetOnMap = centerOnMap - newViewportSizeOnMap / 2;
				
				//4. Find the new offset in UI coordinates and scroll to it
				var newOffsetOnUI = newOffsetOnMap * newTransform.Value;
				this.ScrollToHorizontalOffset(newOffsetOnUI.X);
				this.ScrollToVerticalOffset(newOffsetOnUI.Y);
			}
			
			//Finally, apply zoom to map
			this.VisualHost.LayoutTransform = newTransform;
			this.VisualHost.InvalidateToolTip();
		}
	}
}
